API GatewayのVTLテンプレートだけでタイムゾーンAsia/TokyoでISO8601形式の文字列を生成する曲芸に挑戦してみた
リテールアプリ共創部@大阪の岩田です
API GatewayのAWS統合を使うとLambdaを用いなくてもAPI GatewayとAWSのサービスを簡単に統合できます。さらにVTLテンプレートを活用することで、動的なパラメータの設定などある程度柔軟に制御することができます。
とある趣味プロジェクトでAPI GatewayのAWS統合を使ってDynamoDBのPutItemを呼び出す際にISO8601形式の日付文字列を動的に生成する必要が出てきたのですが、かなり実装に苦戦したので最終的なVTLテンプレートの中身や注意点などここに供養したいと思います。
VTLテンプレート
最終的なVTLテンプレートは以下の通りです。以下サイトで紹介されているコードをVTLテンプレートに書き換えた形になります。
【寄り道】UNIXTIMEを普通の日付に変換するためのプログラム - その漫画自炊オタクはImageJマクロに恋をする
ロジックの中身などは上記サイトがさらに参考にしている以下のサイトで解説されてたので、興味がある方はそちらを参照して下さい。
#set($dummyIntClass = 0)
#set($SECONDS_IN_A_DAY = 24 * 60 * 60)
#set($UNIX_EPOCH_DAY = 1969 * 365 + 492 - 19 + 4 + 306)
#set($YEAR_ONE = 365)
#set($YEAR_FOUR = $YEAR_ONE * 4 + 1)
#set($YEAR_100 = $YEAR_FOUR * 25 - 1)
#set($YEAR_400 = $YEAR_100 * 4 + 1)
#set($monthday = [0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337])
#set($GMT_TOKYO = 9 * 60 * 60)
#set($unixtime = ($context.requestTimeEpoch / 1000))
#set($unixtime = $unixtime + $GMT_TOKYO)
#set($second = ($unixtime % 60))
#set($minute = ($unixtime / 60 % 60))
#set($hour = ($unixtime / 3600 % 24))
#set($leap = 0)
#set($unixday = ($unixtime / $SECONDS_IN_A_DAY))
#set($splitted = $unixday.toString().split("\."))
#set($unixday = $dummyIntClass.parseInt($splitted[0]))
#set($unixday = $unixday + $UNIX_EPOCH_DAY)
#set($tmp = ($unixday / $YEAR_400))
#set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
#set($year = 400 * $tmp)
#set($unixday = $unixday % $YEAR_400)
#set($tmp = $unixday / $YEAR_100)
#set($n = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
#set($year = $year + $n * 100)
#set($unixday = $unixday % $YEAR_100)
#if($n == 4)
#set($leap = 1)
#else
#set($tmp = $unixday / $YEAR_FOUR)
#set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
#set($year = $year + 4 * $tmp)
#set($unixday = $unixday % $YEAR_FOUR)
#set($tmp = $unixday / $YEAR_ONE)
#set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
#set($n = $tmp)
#set($year = $year + $n)
#set($unixday = $unixday % $YEAR_ONE)
#if($n == 4)
#set($leap = 1)
#end
#end
#if($leap != 0)
#set($month = 2)
#set($day = 29)
#else
#set($tmp = ($unixday + 0.4) * 5 / 153)
#set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
#set($month = $tmp)
#set($day = $unixday - $monthday.get($month) + 1)
#set($month = $month + 3)
#if($month > 12)
#set($year = $year + 1)
#set($month = $month - 12)
#end
#end
#if($month.toString().length() == 1)
#set($month = "0" + $month.toString())
#else
#set($month = $month.toString())
#end
#if($day.toString().length() == 1)
#set($day = "0" + $day.toString())
#else
#set($day = $day.toString())
#end
#if($hour.toString().length() == 1)
#set($hour = "0" + $hour.toString())
#else
#set($hour = $hour.toString())
#end
#if($minute.toString().length() == 1)
#set($minute = "0" + $minute.toString())
#else
#set($minute = $minute.toString())
#end
#if($second.toString().length() == 1)
#set($second = "0" + $second.toString())
#else
#set($second = $second.toString())
#end
#set($isoDate = $year.toString() + "-" + $month + "-" + $day + "T" + $hour + ":" + $minute.toString() + ":" + $second.toString() + "+09:00")
{
"date": "$isoDate"
}
苦労した点
ここからは色々と苦労/工夫した点について紹介したいと思います。
App SyncのVTLテンプレートで実現できる処理がAPI Gatewayだと未対応
App SyncのVTLテンプレートだと$util
を使うことで日時関連の処理も簡単に実現可能です。
App Syncのドキュメントでは例として以下のような実装が紹介されています。
$util.time.nowISO8601() : 2018-02-06T19:01:35.749Z
$util.time.nowEpochSeconds() : 1517943695
$util.time.nowEpochMilliSeconds() : 1517943695750
$util.time.nowFormatted("yyyy-MM-dd HH:mm:ssZ") : 2018-02-06 19:01:35+0000
$util.time.nowFormatted("yyyy-MM-dd HH:mm:ssZ", "+08:00") : 2018-02-07 03:01:35+0800
$util.time.nowFormatted("yyyy-MM-dd HH:mm:ssZ", "Australia/Perth") : 2018-02-07 03:01:35+0800
$util.time の日時ヘルパー - AWS AppSync
しかしAPI GatewayのVTLだと$util
で利用可能な処理は以下の6つだけです。
$util.escapeJavaScript()
$util.parseJson()
$util.urlEncode()
$util.urlDecode()
$util.base64Encode()
$util.base64Decode()
日付/日時に関連する便利な処理は提供されていないので、全て自前で実装する必要があります。
タイムゾーンをAsia/Tokyoにしたい
ISO8601形式の文字列を生成するだけであれば以下Stack Overflowの記事に実装例が見つかりました。
内容的にも理解しやすくて良かったのですが、この実装例だとUTCでの日付文字列になってしまいます。タイムゾーンをAsia/Tokyoにするには$context.requestTimeEpoch
で取得したユニックスタイムスタンプをISO8601形式に変換する必要がありました。
マクロが使えなかった
VTLテンプレート内で何度も繰り返す処理を共通化したかったのですが、#macro
は使えないようでした。ということで冗長ですが何度も同じような処理を記述する必要があります。
Apache Velocity Engine - User Guide
文字列を数値にキャストするために一手間必要
VTLだと$convert
を使って型変換が可能なようですが、API GatewayのVTLでは$convert
が使えませんでした。
Apache Velocity Tools Usage Summary
ということで、#set($dummyIntClass = 0)
で宣言したダミーの変数を使って$dummyIntClass.parseInt("1")
のようにparseInt
を呼び出すことで文字列から数値へのキャストを実現しました。
math.floor的な処理が使えない
API GatewayのVTLでは$math
も使えませんでした。
Apache Velocity Tools Usage Summary
ということで以下のような形で切り捨て処理も自前で実装しました。
#set($tmp = $unixday / $YEAR_FOUR)
#set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
割り算に実行結果を文字列にキャストした後に.
で分割してから1要素目を取得し、また数値にキャストすることで切り捨て処理を実現しています。
0詰めが面倒
日や時間を2桁に0詰めする簡単な処理が無さそうだったので、以下のように文字列長をチェックして1桁なら頭に"0"を付与するという処理を愚直に繰り返しています。
#if($second.toString().length() == 1)
#set($second = "0" + $second.toString())
#else
#set($second = $second.toString())
#end
他に良いやり方をご存知の方がいれば教えてください
まとめ
Lambdaを使いましょう